/*
 * Copyright Hundsun Technologies Inc. All Rights Reserved.
 */

package miner

import (
	"context"
	"encoding/hex"
	"time"

	"hundsun.com/hsl/hschain/protos/config"

	"github.com/golang/protobuf/proto"
	"go.uber.org/atomic"
	"hundsun.com/hsl/hschain/common/log/logging"
	commontypes "hundsun.com/hsl/hschain/common/types"
	"hundsun.com/hsl/hschain/consensus/types"
	"hundsun.com/hsl/hschain/protos"
	"hundsun.com/hsl/hschain/protos/common"
	pbledger "hundsun.com/hsl/hschain/protos/ledger"
)

var emptyBlock = &pbledger.Block{}

// Producer 负责出块
type Producer struct {
	module *SoloMiner
	ctx    context.Context

	latestCreatedBlock atomic.Value
}

func (p *Producer) init(producerConfig *types.ProducerConfig) error {
	shouldProceed := func() bool {
		conType := p.module.baseClient.GetCurParam().GetConsensus().GetType()
		switch conType {
		case config.SoloConsensusType:
			currentMinerAddr := p.module.baseClient.GetCurParam().GetConsensus().GetSoloPayload().GetAddress()
			moduleCfgMiner := string(p.module.MinerAccount.GetAddress())
			if currentMinerAddr != moduleCfgMiner {
				p.module.Log.Infof("abort producing due to current miner %s different from module config miner %s", currentMinerAddr, moduleCfgMiner)
				return false
			}
			return true
		default:
			p.module.Log.Warnf("abort producing due to consensus type already update to %s", conType)
			return false
		}
	}

	beforeBlockCreatedFunc := func() (lastBlockNumber uint64, lastBlockHash protos.Hash) {
		latestCreatedBlock := p.latestCreatedBlock.Load().(*pbledger.Block)
		if latestCreatedBlock.Header == emptyBlock.Header {
			lastBlockCheckedMsg := p.module.baseClient.GetLastBlockCheckedMessage()
			lastBlockNumber = lastBlockCheckedMsg.BlockNumber
			lastBlockHash = lastBlockCheckedMsg.BlockHash
			p.module.Log.Tracef("try to produce with prev block[number='%d',lastBlockHash=%s] in BlockCheckedMessage", lastBlockNumber, hex.EncodeToString(lastBlockHash))
		} else {
			lastBlockNumber = latestCreatedBlock.Header.Number
			lastBlockHash = latestCreatedBlock.Hash()
			p.module.Log.Tracef("try to produce with  prev block[number='%d',lastBlockHash=%s] in BlocksBuffer", lastBlockNumber, hex.EncodeToString(lastBlockHash))
		}
		return
	}

	afterBlockCreatedFunc := func(newBlock *pbledger.Block) {
		newBlockHash := newBlock.Hash()
		blkLog := p.module.Log.WithFields(map[string]interface{}{
			logging.LogFieldBlockHash:   newBlockHash.GetShortString(),
			logging.LogFieldBlockNumber: newBlock.Header.Number,
		})

		p.latestCreatedBlock.Store(newBlock)
		err := p.module.baseClient.WriteBlock(newBlock)
		if err != nil {
			blkLog.Errorf("dispatched new block to blockchain module failed, err:%s\n.", err.Error())
		}
		blkLog.Info("dispatched new block to blockchain module.")

		err = p.module.baseClient.BroadCastBlock(newBlock, 5)
		if err != nil {
			blkLog.Error("broadcast block failed, err: %s\n", err.Error())
		}
	}
	err := p.module.baseClient.InitProducer(producerConfig, shouldProceed, beforeBlockCreatedFunc, afterBlockCreatedFunc)
	if err != nil {
		return err
	}

	return nil
}

//Run 出块
func (p *Producer) Run() {
	p.module.Log.Infof("start solo producer...")
	defer p.module.Log.Info("exit solo producer ...")

	for {
		select {
		case <-time.After(time.Second * 3):
			p.produceBlocks()
		case <-p.ctx.Done():
			return
		}
	}
}

func (p *Producer) produceBlocks() {
	p.module.Log.Info("produceBlocks entry...")
	defer p.module.Log.Info("produceBlocks exit...")
	//出块前准备工作
	p.latestCreatedBlock.Store(emptyBlock)
	var latestBlockCheckedMsg *common.BlockCheckedMessage
	for latestBlockCheckedMsg == nil {
		//等几秒，尽可能让miner切换时，旧miner发出的块能被序列号中心保存
		time.Sleep(time.Second * 6)
		latestBlockCheckedMsg = p.GetLatestBlockCheckedMessageIfSynchronized()
		p.module.baseClient.SetLastBlockCheckedMessage(latestBlockCheckedMsg)
	}
	p.module.baseClient.ProduceBlocks()
}

// GetLatestBlockCheckedMessageIfSynchronized 如果已校验的区块的序列号和序列号中心的序列号一致，则返回最新的BlockCheckedMessage；否则，返回nil
func (p *Producer) GetLatestBlockCheckedMessageIfSynchronized() *common.BlockCheckedMessage {
	maxCheckedBlock, err := p.module.baseClient.RequestTopicInfo(commontypes.EventQueryCheckedBlockTopic)
	if err != nil {
		p.module.Log.Errorf("failed to get latest BlockCheckedMessage, err:%s", err.Error())
		return nil
	}

	var chkMsg common.BlockCheckedMessage
	err = proto.Unmarshal(maxCheckedBlock, &chkMsg)
	if err != nil {
		p.module.Log.Warnf("failed to unmarshal latest BlockCheckedMessage:%s", err.Error())
		return nil
	}

	return &chkMsg
}
